"use client"; import { useState, useEffect, useCallback } from "react"; export type AnnotationColor = "amber" | "rose" | "sky" | "emerald"; export interface Annotation { id: string; slug: string; chapterIndex: number; chapterTitle: string; selectedText: string; note: string; color: AnnotationColor; createdAt: number; } export const ANNO_COLOR_MAP: Record = { amber: { bg: "rgba(251,191,36,0.22)", border: "#f59e0b", dot: "#f59e0b" }, rose: { bg: "rgba(251,113,133,0.22)", border: "#f43f5e", dot: "#f43f5e" }, sky: { bg: "rgba(56,189,248,0.22)", border: "#0ea5e9", dot: "#0ea5e9" }, emerald: { bg: "rgba(52,211,153,0.22)", border: "#10b981", dot: "#10b981" }, }; const STORAGE_KEY = (slug: string) => `nx-ann-${slug}`; export function loadAnnotations(slug: string): Annotation[] { if (typeof window === "undefined") return []; try { return JSON.parse(localStorage.getItem(STORAGE_KEY(slug)) ?? "[]"); } catch { return []; } } export function saveAnnotation(ann: Annotation): void { const all = loadAnnotations(ann.slug).filter(a => a.id !== ann.id); localStorage.setItem(STORAGE_KEY(ann.slug), JSON.stringify([...all, ann])); } export function deleteAnnotation(slug: string, id: string): void { const all = loadAnnotations(slug); localStorage.setItem(STORAGE_KEY(slug), JSON.stringify(all.filter(a => a.id !== id))); } export function updateAnnotationNote(slug: string, id: string, note: string): void { const all = loadAnnotations(slug).map(a => a.id === id ? { ...a, note } : a); localStorage.setItem(STORAGE_KEY(slug), JSON.stringify(all)); } // ── Theme ───────────────────────────────────────────────────────────────────── interface Theme { bg: string; text: string; muted: string; border: string; chrome: string; chromeBorder: string; accent: string; } interface PanelProps { slug: string; open: boolean; onClose: () => void; t: Theme; fontFamily:string; } // ── AnnotationsPanel ───────────────────────────────────────────────────────── export function AnnotationsPanel({ slug, open, onClose, t, fontFamily }: PanelProps) { const [anns, setAnns] = useState([]); const [editingId, setEditingId] = useState(null); const [editNote, setEditNote] = useState(""); const reload = useCallback(() => { setAnns(loadAnnotations(slug).sort((a, b) => a.createdAt - b.createdAt)); }, [slug]); useEffect(() => { if (open) { reload(); setEditingId(null); } }, [open, reload]); const handleDelete = (id: string) => { deleteAnnotation(slug, id); reload(); }; const startEdit = (ann: Annotation) => { setEditingId(ann.id); setEditNote(ann.note); }; const commitEdit = (id: string) => { updateAnnotationNote(slug, id, editNote.trim()); setEditingId(null); reload(); }; // Group by chapter index const grouped = anns.reduce>((acc, ann) => { const k = String(ann.chapterIndex); (acc[k] ??= []).push(ann); return acc; }, {}); const groupKeys = Object.keys(grouped).sort((a, b) => Number(a) - Number(b)); const iconBtn = (label: string, onClick: () => void, d: string) => ( ); return ( <> {/* Backdrop */}
{/* Slide-in panel */}
{/* Header */}
Annotations {anns.length > 0 && ( {anns.length} )}
{/* Scrollable list */}
{anns.length === 0 ? (

No annotations yet

Tap the pencil icon, then select any text to highlight it.

) : (
{groupKeys.map(gk => { const group = grouped[gk]; const first = group[0]; return (
{/* Chapter label */}

Ch {first.chapterIndex + 1} · {first.chapterTitle}

{group.map(ann => { const c = ANNO_COLOR_MAP[ann.color]; const isEditing = editingId === ann.id; return (
{/* Top row: action buttons */}
{iconBtn("Edit note", () => startEdit(ann), "M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" )} {iconBtn("Delete annotation", () => handleDelete(ann.id), "M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6" )}
{/* Quoted text */}

"{ann.selectedText}"

{/* Note area */} {isEditing ? (